package org.hilo.boot.core.dubbo;

import com.alibaba.dubbo.rpc.*;
import com.alibaba.dubbo.rpc.service.GenericService;
import org.jretty.apibase.MsgBase;
import org.hilo.boot.core.RefMsg;
import org.hilo.boot.core.UT;
import org.jretty.log.LogFactory;
import org.jretty.util.I18nExceptionSupport;

/**
 * 推荐的配置为：（建议hprovider放在前面，越前面的filter越先执行、越后执行完）
 *  dubbo.provider.filter = hprovider,default,-exception,-timeout
 *  dubbo.consumer.filter = hconsumer,default
 * 
 * hprovider：  添加本filter
 * -exception： Dubbo自带的ExceptionFilter作用于Provider，我们将其去掉。
 * 
 * Dubbo filter配置讲解及源码分析，参见：https://www.cnblogs.com/ceshi2016/p/12016141.html
 * 
 * dubbo原生的filter调用链，优先于自定义的filter执行，原生filters如下：
echo=com.alibaba.dubbo.rpc.filter.EchoFilter
generic=com.alibaba.dubbo.rpc.filter.GenericFilter
genericimpl=com.alibaba.dubbo.rpc.filter.GenericImplFilter
token=com.alibaba.dubbo.rpc.filter.TokenFilter
accesslog=com.alibaba.dubbo.rpc.filter.AccessLogFilter
activelimit=com.alibaba.dubbo.rpc.filter.ActiveLimitFilter
classloader=com.alibaba.dubbo.rpc.filter.ClassLoaderFilter
context=com.alibaba.dubbo.rpc.filter.ContextFilter
consumercontext=com.alibaba.dubbo.rpc.filter.ConsumerContextFilter
exception=com.alibaba.dubbo.rpc.filter.ExceptionFilter
executelimit=com.alibaba.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=com.alibaba.dubbo.rpc.filter.DeprecatedFilter
compatible=com.alibaba.dubbo.rpc.filter.CompatibleFilter
timeout=com.alibaba.dubbo.rpc.filter.TimeoutFilter
monitor=com.alibaba.dubbo.monitor.support.MonitorFilter
validation=com.alibaba.dubbo.validation.filter.ValidationFilter
cache=com.alibaba.dubbo.cache.filter.CacheFilter
trace=com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter
future=com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter
 * 
 * 原生filter，通常是直接 throw new RpcException。
 * 
 * 越前面的filter越先执行、越后执行完，所以EchoFilter要先执行，拦截到echo请求后直接return，
 * 另外，TokenFilter先于ExceptionFilter执行，意味着Token不匹配，将直接throw new RpcException，
 * 后执行的ExceptionFilter是可以捕获到该异常的。
 * 
 * 为了与官方filter不冲突，我们需要了解每个官方filter的作用。
 * 
 * 
 * 我们的自定义filter，是放在官方filter之后执行的，所以先执行完，而TokenFilter、ActiveLimitFilter后执行完，
 * 这显然达不到监控的效果。所以我们的filter，必须要两头控制，
 * 一头（firstFilter）最先执行、最后执行完，另一头（lastFilter）最后执行、最先执行完。
 * 相对来说，firstFilter更为重要，可以得到其他filter执行后的结果，可以用来做监控和异常处理。
 * 
 * @author zollty
 * @since 2020年6月10日
 */
//@Activate(group = Constants.PROVIDER) // 默认启用
public class ProviderMainFilter extends AbstractFilterService implements Filter {
    protected boolean traceAll;
    
    public ProviderMainFilter() {
        super(LogFactory.getLogger("Dubbo_Provider_Filter_Log"));
    }

    /**
     * 当配置为：
     *  dubbo.provider.filter = hprovider,default,....
     * 时，该invoke为第一个执行，最后一个执行完。
     * 
     * 核心设计：provider.filter参照dubbo原生 ExceptionFilter，处理逻辑如下：
     * 异常有两大类：
     * 一种是最底层invoke的dubbo service方法暴的异常，另一种是执行filter过程中附加的RpcException或者未知的RuntimeException。
     * 
     * 针对filter链自身的异常（RpcException及RuntimeException），我们不做处理，原样返回，但是要记录好日志。
     * 针对dubbo service抛出的异常，我们捕获到然后转换成统一的error result格式（apibase.Result.fail(code, ex.getMessage())）
     * 
     * 在consumer.filter端，就要反向操作：
     *  如果收到RpcException，则通常是自身filter或者对方filter暴出的异常。要记录日志，并转换成error result格式。
     *  如果收到error result格式，则原样返回，但是在metrics监控侧，可以记录该异常信息。
     */
    @Override
    public Result invoke(Invoker<?> invoker, Invocation inv) {
        Long startTime = System.currentTimeMillis();
        String remoteHost = RpcContext.getContext().getRemoteHost();
        Result result;
        try {
            result = invoker.invoke(inv);
        } catch (RuntimeException ex) {
            // dubbo自身异常，原样返回，但记录日志。
            handleDubboException(invoker, inv, remoteHost, startTime, ex);
            // directly throw if it's dubbo exception
            throw ex;
        }
        if (GenericService.class == invoker.getInterface()) {
            return result;
        }

        // dubbo service异常，转换成统一的error result格式（apibase.Result.fail(code, ex.getMessage())）
        if (result.hasException()) {

            result = handleServiceException(invoker, inv, remoteHost, startTime, result);

        } else {// 执行成功

            long elapsed = System.currentTimeMillis() - startTime;
            if (invoker.getUrl() != null && elapsed > invoker.getUrl().getMethodParameter(inv.getMethodName(),
                    "timeout", Integer.MAX_VALUE)) {
                // 执行成功，但执行超时

                recordMetrics(invoker, inv, remoteHost, startTime, DubboStatus.DUBBO_ERROR, DubboStatus.TIMEOUT.name());

                traceException(invoker, inv, remoteHost, startTime, DubboStatus.TIMEOUT.name(),
                        "invoke service method timeout！");

            } else {
                // 执行成功！！

                if (traceAll) {
                    traceNormal(invoker, inv, remoteHost, startTime, result);
                }

                recordMetricsSuccess(false, invoker, inv, remoteHost, startTime);
            }

        }

        return result;
    }
    
    protected void handleDubboException(Invoker<?> invoker, Invocation inv, String remoteHost, Long startTime,
            Throwable ex) {
        String code;
        if (ex instanceof RpcException) {
            code = DubboStatus.getByException((RpcException) ex).name();
        } else {
            code = DubboStatus.UNKNOWN.name();
        }

        String key = UT.Excp.getExceptionSign(ex);

        checkAndLog(invoker, inv, remoteHost, startTime, key, code, ex);

        recordMetrics(invoker, inv, remoteHost, startTime, DubboStatus.DUBBO_ERROR, code);
    }
    
    protected Result handleServiceException(Invoker<?> invoker, Invocation inv, String remoteHost, Long startTime,
            Result result) {
        Throwable ex = result.getException();
        org.jretty.apibase.Result<?> svcResult;
        String code;
        if (ex instanceof I18nExceptionSupport) {
            code = ((I18nExceptionSupport) ex).getErrorCode();
            if (UT.Str.isNullOrEmpty(code)) {
                code = MsgBase.UNDEFINED_ERR.name();
            }
            svcResult = org.jretty.apibase.Result.fail(code, ex.getMessage());

            if (traceAll) {
                traceException(invoker, inv, remoteHost, startTime, code, svcResult.getMessage());
            }

        } else { // 未捕获异常，自动记录日志
            String key = UT.Excp.getExceptionSign(ex);
            String msg = UT.Excp.getStackTraceStr(DubboStackTraceFilter.me(), ex);
            code = cache.getData(key);
            if (code == null) {
                code = new RefMsg().getCode();
                checkAndLog(invoker, inv, remoteHost, startTime, key, code, msg);
            }
            svcResult = org.jretty.apibase.Result.fail(code, msg);
            code = ex.getClass().getSimpleName();
            svcResult.put("error", code);
        }

        recordMetrics(invoker, inv, remoteHost, startTime, DubboStatus.BIZ_ERROR, code);
        return new RpcResult(svcResult);
    }
    
    /**
     * 记录异常日志 如果最近5秒内有这个相同的异常，则不再记录。5秒钟后，重新计时。
     */
    protected void checkAndLog(Invoker<?> invoker, Invocation inv, String remoteHost, Long startTime, String key,
            String code, Throwable ex) {
        if (cache.putIfAbsent(key, code)) {
            StringBuilder sn = assembleBaseInfo(invoker, inv, remoteHost, startTime).insert(0, "error: ")
                    .append(": \r\n").append(code).append(": ")
                    .append(UT.Excp.getStackTraceStr(DubboStackTraceFilter.me(), ex));

            log.error(sn.toString());
        }
    }

    protected void recordMetrics(Invoker<?> invoker, Invocation inv, String remoteHost, long start, String outcome,
            String errorCode) {
        super.recordMetrics(false, invoker, inv, remoteHost, start, outcome, errorCode);
    }

}
